/* * Copyright (c) 2008 Stiftung Deutsches Elektronen-Synchrotron, * Member of the Helmholtz Association, (DESY), HAMBURG, GERMANY. * * THIS SOFTWARE IS PROVIDED UNDER THIS LICENSE ON AN "../AS IS" BASIS. * WITHOUT WARRANTY OF ANY KIND, EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED * TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR PARTICULAR PURPOSE AND * NON-INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE * FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR * THE USE OR OTHER DEALINGS IN THE SOFTWARE. SHOULD THE SOFTWARE PROVE DEFECTIVE * IN ANY RESPECT, THE USER ASSUMES THE COST OF ANY NECESSARY SERVICING, REPAIR OR * CORRECTION. THIS DISCLAIMER OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. * NO USE OF ANY SOFTWARE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER. * DESY HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, * OR MODIFICATIONS. * THE FULL LICENSE SPECIFYING FOR THE SOFTWARE THE REDISTRIBUTION, MODIFICATION, * USAGE AND OTHER RIGHTS AND OBLIGATIONS IS INCLUDED WITH THE DISTRIBUTION OF THIS * PROJECT IN THE FILE LICENSE.HTML. IF THE LICENSE IS NOT INCLUDED YOU MAY FIND A COPY * AT HTTP://WWW.DESY.DE/LEGAL/LICENSE.HTM */ package org.csstudio.sds.components.ui.internal.figures; import java.text.NumberFormat; import java.util.Arrays; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import org.csstudio.sds.ui.figures.BorderAdapter; import org.csstudio.sds.ui.figures.CrossedOutAdapter; import org.csstudio.sds.ui.figures.IBorderEquippedWidget; import org.csstudio.sds.ui.figures.ICrossedFigure; import org.csstudio.sds.ui.figures.IRhombusEquippedWidget; import org.csstudio.sds.ui.figures.RhombusAdapter; import org.csstudio.sds.util.ChannelReferenceValidationException; import org.csstudio.sds.util.ChannelReferenceValidationUtil; import org.eclipse.core.runtime.IAdaptable; import org.eclipse.draw2d.ColorConstants; import org.eclipse.draw2d.Figure; import org.eclipse.draw2d.FigureListener; import org.eclipse.draw2d.Graphics; import org.eclipse.draw2d.IFigure; import org.eclipse.draw2d.Label; import org.eclipse.draw2d.Panel; import org.eclipse.draw2d.PositionConstants; import org.eclipse.draw2d.RectangleFigure; import org.eclipse.draw2d.ToolbarLayout; import org.eclipse.draw2d.XYLayout; import org.eclipse.draw2d.geometry.Insets; import org.eclipse.draw2d.geometry.Point; import org.eclipse.draw2d.geometry.PointList; import org.eclipse.draw2d.geometry.Rectangle; import org.eclipse.swt.graphics.Color; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * <p>Base class for widgets that implement a chart (waveform, strip chart).</p> * * <p>This class paints the plot, labels and axes. It is also responsible for * transforming data values to display coordinates and can autoscale the y-axis * if requested by the user.</p> * * <p>Subsclasses of this class are responsible for managing the actual data * values to be displayed in the plot. They must provide these values to this * base class by implementing the {@link #dataValues} method. If the data range * of the y-axis or the x-axis changes, subclasses must call the methods * {@link #dataRangeChanged} or {@link #xAxisRangeChanged}, respectively.</p> * * @author Joerg Rathlev * @author based on waveform by Kai Meyer and Sven Wende */ public abstract class AbstractChartFigure extends Figure implements IAdaptable { // TODO: format all comments // TODO: check all method names for "waveform" etc. /** * Constant value which represents that a scale or grid lines should be * shown for the x-axis. */ private static final int SHOW_X_AXIS = 1; /** * Constant value which represents that a scale or grid lines should be * shown for the y-axis. */ private static final int SHOW_Y_AXIS = 2; /** * Constant value which represents that a scale or grid lines should be * shown for both axes. */ private static final int SHOW_BOTH = 3; /** * The size of an axis with ticks in pixels. For a horizontal axis, this is * the height of the axis; for a vertical axis, this is its width. */ private static final int AXIS_SIZE = 10; /** * Height of the text. */ private static final int TEXTHEIGHT = 14; /** * The width of the area that is reserved for the axis labels to the left * of the y-axis. */ private static final int TEXTWIDTH = 46; /** * A rectangle with zero width and height. */ private static final Rectangle ZERO_RECTANGLE = new Rectangle(0, 0, 0, 0); /** * A border adapter, which covers all border handlings. */ private IBorderEquippedWidget _borderAdapter; /** * Whether this figure has a transparent background. */ private boolean _transparent; /** * The axes for which grid lines are drawn. * @see #SHOW_X_AXIS * @see #SHOW_Y_AXIS * @see #SHOW_BOTH */ private int _showGridLines = 0; /** * Which axes are shown. * @see #SHOW_X_AXIS * @see #SHOW_Y_AXIS * @see #SHOW_BOTH */ private int _showAxes = 0; /** * Whether the axes ticks are labeled. */ private boolean _labeledTicks = true; /** * The maximum data value set in this waveform's properties. */ private double _propertyMax = 0; /** * The minimum data value set in this waveform's properties. */ private double _propertyMin = 0; /** * Whether autoscaling is enabled. */ private boolean _autoscale = false; /** * The plot colors for the data arrays. */ private final Color[] _plotColor; /** * Whether the plot is enabled. */ private final boolean[] _plotEnabled; /** * Whether this figure is drawn as a line chart. */ private boolean _lineChart = false; /** * <code>true</code> until the figure is painted for the first time. This is * used to prevent recalculating the layout of the subfigures while the * properties are set initially. */ private boolean _deferLayout = true; /** * The number of data series displayed by this figure. */ private final int _numberOfDataSeries; /** * The bounds of the plotting area (where the data points are drawn). * The location of the rectangle is relative to the figure bounds. */ private Rectangle _plotBounds = new Rectangle(10, 10, 10, 10); /** * The y-axis data mapping. */ private IAxis _yAxis = new LinearAxis(0.0, 0.0, 0); /** * The x-axis data mapping. */ private final IAxis _xAxis = new LinearAxis(0.0, 0.0, 0); /** * The scale for the x-axis. */ private Scale _xAxisScale; /** * The scale for the y-axis. */ private Scale _yAxisScale; /** * The scale for x-axis grid lines. */ private Scale _xAxisGridLines; /** * The scale for y-axis grid lines. */ private Scale _yAxisGridLines; /** * The graph of this waveform. */ private PlotFigure _plotFigure; /** * The label for this waveform. */ private Label _waveformLabel; /** * The x-axis label. */ private Label _xAxisLabel; /** * The y-axis label. */ private Label _yAxisLabel; /** * The logger used by this object. */ private static final Logger _logger = LoggerFactory.getLogger(AbstractChartFigure.class); /** * The aliases of this waveform. */ private Map<String, String> _aliases; private ICrossedFigure _crossedOutAdapter; private IRhombusEquippedWidget _rhombusAdapter; /** * Creates a new chart figure. * * @param numberOfDataSeries * the number of data series to be displayed by this figure. */ protected AbstractChartFigure(final int numberOfDataSeries) { if (numberOfDataSeries < 0) { throw new IllegalArgumentException("numberOfDataSeries must be >= 0"); } _numberOfDataSeries = numberOfDataSeries; _plotColor = new Color[_numberOfDataSeries]; Arrays.fill(_plotColor, ColorConstants.black); _plotEnabled = new boolean[_numberOfDataSeries]; Arrays.fill(_plotEnabled, true); setLayoutManager(new XYLayout()); createSubfigures(); addRefreshLayoutListener(); } @Override public void paint(final Graphics graphics) { super.paint(graphics); Rectangle bound = getBounds().getCopy(); _crossedOutAdapter.paint(graphics); _rhombusAdapter.paint(graphics); } /** * Registers a figure listener that listens for movement events and * refreshes the layout of the subfigures when the figure has moved. */ private void addRefreshLayoutListener() { addFigureListener(new FigureListener() { @Override public void figureMoved(final IFigure source) { AbstractChartFigure.this.refreshConstraints(); } }); } /** * Creates the subfigures that this figure contains. */ private void createSubfigures() { _yAxisGridLines = new Scale(); _yAxisGridLines.setHorizontalOrientation(false); _yAxisGridLines.setShowValues(false); _yAxisGridLines.setForegroundColor(ColorConstants.lightGray); this.add(_yAxisGridLines); _xAxisGridLines = new Scale(); _xAxisGridLines.setHorizontalOrientation(true); _xAxisGridLines.setShowValues(false); _xAxisGridLines.setForegroundColor(ColorConstants.lightGray); this.add(_xAxisGridLines); _yAxisScale = new Scale(); _yAxisScale.setHorizontalOrientation(false); _yAxisScale.setShowValues(_labeledTicks); _yAxisScale.setAlignment(true); _yAxisScale.setForegroundColor(this.getForegroundColor()); this.add(_yAxisScale); _xAxisScale = new Scale(); _xAxisScale.setHorizontalOrientation(true); _xAxisScale.setShowValues(_labeledTicks); _xAxisScale.setAlignment(false); _xAxisScale.setForegroundColor(this.getForegroundColor()); this.add(_xAxisScale); _plotFigure = new PlotFigure(); this.add(_plotFigure); _waveformLabel = new Label(""); this.add(_waveformLabel); _xAxisLabel = new Label(""); this.add(_xAxisLabel); _yAxisLabel = new Label(""); this.add(_yAxisLabel); /* TODO: Draw y-label text vertically. * * The following way to do this is recommended in the Eclipse * newsgroups, but it currently causes a NullPointerException: * * _yAxisLabel = new Label("") { * protected void paintFigure(...) { * graphics.rotate(90); * super.paintFigure(...); * } * } * * http://dev.eclipse.org/newslists/news.eclipse.tools.gef/msg15609.html * http://dev.eclipse.org/mhonarc/newsLists/news.eclipse.tools.gef/msg20487.html */ } /** * Returns the lowest data value. * * @return the lowest data value. */ protected abstract double lowestDataValue(); /** * Returns the greatest data value. * * @return the greatest data value. */ protected abstract double greatestDataValue(); /** * Sends the data points of the data series with the specified index to the * processor. * * @param index * the index of the data series. * @param processor * the processor. */ protected abstract void dataValues(int index, IDataPointProcessor processor); /** * Returns the lowest x-axis value. * * @return the lowest x-axis value. */ protected abstract double xAxisMinimum(); /** * Returns the greatest x-axis value. * * @return the greatest x-axis value. */ protected abstract double xAxisMaximum(); /** * Notifies this figure that the range of the x-axis has changed. This * method must be called by subclasses when the range of the x-axis has * changed. */ protected final void xAxisRangeChanged() { _logger.debug("xAgisRangeChanged(): " + xAxisMinimum() + ", " + xAxisMaximum()); _xAxis.setDataRange(xAxisMinimum(), xAxisMaximum()); refreshConstraints(); } /** * Notifies this figure that the data range has changed. This method must * be called by subclasses when the lowest or greates data value has * changed. */ protected final void dataRangeChanged() { if (_autoscale) { _yAxis.setDataRange(lowestDataValue(), greatestDataValue()); refreshConstraints(); } } /** * Returns the lower bound of the y-axis. * * @return the lower bound of the y-axis. */ private double yAxisLowerBound() { return _autoscale ? lowestDataValue() : _propertyMin; } /** * Returns the upper bound of the y-axis. * * @return the upper bound of the y-axis. */ private double yAxisUpperBound() { return _autoscale ? greatestDataValue() : _propertyMax; } /** * {@inheritDoc} */ @Override public final Object getAdapter(final Class adapter) { if (adapter == IBorderEquippedWidget.class) { if (_borderAdapter == null) { _borderAdapter = new BorderAdapter(this); } return _borderAdapter; } else if(adapter == ICrossedFigure.class) { if(_crossedOutAdapter==null) { _crossedOutAdapter = new CrossedOutAdapter(this); } return _crossedOutAdapter; } else if(adapter == IRhombusEquippedWidget.class) { if(_rhombusAdapter==null) { _rhombusAdapter = new RhombusAdapter(this); } return _rhombusAdapter; } return null; } /** * {@inheritDoc} */ @Override protected final void paintFigure(final Graphics graphics) { super.paintFigure(graphics); // After the background of this figure is painted, its children will // be painted, so if we have not layed them out yet, we must do so now. if (_deferLayout) { _deferLayout = false; this.refreshConstraints(); } } /** * Sets the transparent state of the background. * * @param transparent * the transparent state. */ public final void setTransparent(final boolean transparent) { _transparent = transparent; } /** * {@inheritDoc} */ @Override public final boolean isOpaque() { return !_transparent; } /** * Sets which axes should be displayed. * * @param axes * a value representing which axes should be displayed. * @see #SHOW_X_AXIS * @see #SHOW_Y_AXIS * @see #SHOW_BOTH */ public final void setShowScale(final int axes) { _showAxes = axes; refreshConstraints(); } /** * Sets the axes for which grid lines should be displayed. * * @param axes * a value representing for which axes grid lines should be * displayed. * @see #SHOW_X_AXIS * @see #SHOW_Y_AXIS * @see #SHOW_BOTH */ public final void setShowGridLines(final int axes) { _showGridLines = axes; refreshConstraints(); } /** * Sets the width of the lines of the graph. * @param lineWidth * The width of the lines of the graph. */ public final void setGraphLineWidth(final int lineWidth) { _plotFigure.setPlotLineWidth(lineWidth); } /** * {@inheritDoc} */ @Override public final void setBackgroundColor(final Color backgroundColor) { super.setBackgroundColor(backgroundColor); _plotFigure.setBackgroundColor(backgroundColor); } /** * {@inheritDoc} */ @Override public final void setForegroundColor(final Color foregroundcolor) { super.setForegroundColor(foregroundcolor); _plotFigure.setForegroundColor(foregroundcolor); } /** * Sets the color for the grid lines. * * @param color * the color */ public final void setGridLinesColor(final Color color) { _yAxisGridLines.setForegroundColor(color); _xAxisGridLines.setForegroundColor(color); } /** * Sets, if the values should be shown. * * @param showValues * True, if the values should be shown, false otherwise */ public final void setShowValues(final boolean showValues) { _labeledTicks = showValues; _yAxisScale.setShowValues(showValues); _xAxisScale.setShowValues(showValues); this.refreshConstraints(); } /** * Sets the y-axis scaling of this waveform figure. * * @param scaling * the new scaling. 0 = linear, 1 = logarithmic. */ public final void setYAxisScaling(final int scaling) { switch (scaling) { case 0: default: _yAxis = new LinearAxis(yAxisLowerBound(), yAxisUpperBound(), _plotBounds.height); break; case 1: _yAxis = new LogarithmicAxis(yAxisLowerBound(), yAxisUpperBound(), _plotBounds.height); break; } refreshConstraints(); } /** * Sets the label. * * @param label the label. */ public final void setLabel(final String label) { try { _waveformLabel.setText(ChannelReferenceValidationUtil .createCanonicalName(label, _aliases)); } catch (ChannelReferenceValidationException e) { _waveformLabel.setText(label); _logger.info("Waveform label contains unresolvable aliases: \"" + label + "\""); } refreshConstraints(); } /** * Sets the x-axis label. * * @param axisLabel the label. */ public final void setXAxisLabel(final String axisLabel) { try { _xAxisLabel.setText(ChannelReferenceValidationUtil .createCanonicalName(axisLabel, _aliases)); } catch (ChannelReferenceValidationException e) { _xAxisLabel.setText(axisLabel); _logger.info("Waveform x-axis label contains unresolvable aliases: \"" + axisLabel + "\""); } refreshConstraints(); } /** * Sets the y-axis label. * * @param axisLabel the label. */ public final void setYAxisLabel(final String axisLabel) { try { _yAxisLabel.setText(ChannelReferenceValidationUtil .createCanonicalName(axisLabel, _aliases)); } catch (ChannelReferenceValidationException e) { _yAxisLabel.setText(axisLabel); _logger.info("Waveform y-axis label contains unresolvable aliases: \"" + axisLabel + "\""); } refreshConstraints(); } /** * Sets the aliases of this waveform. * * @param aliases * the aliases of this waveform. */ public final void setAliases(final Map<String, String> aliases) { _aliases = aliases != null ? aliases : new HashMap<String, String>(); } /** * Sets the max value for the graph. * @param max * The max value */ public final void setMax(final double max) { _propertyMax = max; if (!_autoscale) { _yAxis.setDataRange(_propertyMin, _propertyMax); this.refreshConstraints(); } } /** * Sets the min value for the graph. * @param min * The min value */ public final void setMin(final double min) { _propertyMin = min; if (!_autoscale) { _yAxis.setDataRange(_propertyMin, _propertyMax); this.refreshConstraints(); } } /** * Sets, if the graph should be automatically scaled. * @param autoscale * True if it should be automatically scaled, false otherwise */ public final void setAutoScale(final boolean autoscale) { _autoscale = autoscale; if (!_autoscale) { _yAxis.setDataRange(_propertyMin, _propertyMax); } else { _yAxis.setDataRange(lowestDataValue(), greatestDataValue()); } this.refreshConstraints(); } /** * Sets whether the chart is a line chart. * * @param lineChart * <code>true</code> if the chart should be drawn as a line * chart, <code>false</code> otherwise. */ public final void setLineChart(final boolean lineChart) { _lineChart = lineChart; } /** * Sets the data point drawing style. * * @param style the style. */ public final void setDataPointDrawingStyle(final int style) { _plotFigure.setDataPointDrawingStyle(style); refreshConstraints(); } /** * Sets the color to be used for the plot of the data with the specified * index. * * @param index * the data index. This must be a positive integer or zero, and * smaller than the number of data arrays specified in the * constructor of this figure. * @param color * the color. */ public final void setPlotColor(final int index, final Color color) { if ((index < 0) || (index >= _numberOfDataSeries)) { throw new IndexOutOfBoundsException( "invalid index: " + index); } _plotColor[index] = color; } /** * Enables or disables the plot with the specified index. * * @param index * the data index. This must be a positive integer or zero, and * smaller than the number of data arrays specified in the * constructor of this figure. * @param enabled * whether the plot should be enabled. */ public final void setPlotEnabled(final int index, final boolean enabled) { if ((index < 0) || (index >= _numberOfDataSeries)) { throw new IndexOutOfBoundsException( "invalid index: " + index); } _plotEnabled[index] = enabled; } /** * Checks whether the x-axis is displayed. * * @return <code>true</code> if the x-axis is displayed, * <code>false</code> otherwise. */ protected final boolean showXAxis() { return ((_showAxes == SHOW_X_AXIS) || (_showAxes == SHOW_BOTH)); } /** * Checks whether the y-axis is displayed. * * @return <code>true</code> if the y-axis is displayed, * <code>false</code> otherwise. */ protected final boolean showYAxis() { return ((_showAxes == SHOW_Y_AXIS) || (_showAxes == SHOW_BOTH)); } /** * Checks whether gridlines are displayed for the x-axis. * * @return <code>true</code> if gridlines are displayed, * <code>false</code> otherwise. */ protected final boolean showXAxisGrid() { return ((_showGridLines == SHOW_X_AXIS) || (_showGridLines == SHOW_BOTH)); } /** * Checks whether gridlines are displayed for the y-axis. * * @return <code>true</code> if gridlines are displayed, * <code>false</code> otherwise. */ protected final boolean showYAxisGrid() { return ((_showGridLines == SHOW_Y_AXIS) || (_showGridLines == SHOW_BOTH)); } /** * Returns the y position relative to the top of the plot at which the given * value should be drawn. * * @param value * the y value. * @return the y position. */ private int valueToYPos(final double value) { // the data values are mapped to [0, height-1] int plotHeight = _plotBounds.height - 1; // the axis calculates the distance from the lower bound of the data // range, but for the y coordinate, we need the distance from the top // of the plot, so we subtract the returned value from plotHeight. return plotHeight - _yAxis.valueToCoordinate(value); } /** * Returns the x position relative to the left of the plot at which the * given value should be drawn. * * @param value * the x value. This is usually the index of the data point * within the data array. * @return the x position. */ private int valueToXPos(final double value) { return _xAxis.valueToCoordinate(value); } /** * Performs the layout of the subfigures of this figure. */ public final void refreshConstraints() { if (_deferLayout) { return; } Rectangle figBounds = this.getBounds().getCopy(); figBounds.crop(this.getInsets()); // These bounds are used for the placement of the sub-figures below. // The bounds are cropped after the placement of each sub-figure and // the next sub-figure will be placed in the remaining bounds. Rectangle bounds = new Rectangle(0, 0, figBounds.width, figBounds.height); Rectangle labelBounds = calculateLabelBounds(bounds); setConstraint(_waveformLabel, labelBounds); Rectangle xAxisLabelBounds = calculateXAxisLabelBounds(bounds); setConstraint(_xAxisLabel, xAxisLabelBounds); Rectangle xAxisBounds = calculateXAxisBounds(bounds); setConstraint(_xAxisScale, xAxisBounds); Rectangle yAxisLabelBounds = calculateYAxisLabelBounds(bounds); setConstraint(_yAxisLabel, yAxisLabelBounds); Rectangle yAxisBounds = calculateYAxisBounds(bounds); setConstraint(_yAxisScale, yAxisBounds); _plotBounds = calculatePlotBounds(bounds); setConstraint(_plotFigure, _plotBounds); // Grid lines are located on top of the plot (within the same bounds, // but the y-axis grid needs to be adjusted for the text height at the // top to align with the y-axis). setConstraint(_yAxisGridLines, showYAxisGrid() ? _plotBounds.getCopy().expand( new Insets(TEXTHEIGHT / 2, 0, 0 ,0)) : ZERO_RECTANGLE); _yAxisGridLines.setWideness(_plotBounds.width); setConstraint(_xAxisGridLines, showXAxisGrid() ? _plotBounds.getCopy() : ZERO_RECTANGLE); _xAxisGridLines.setWideness(_plotBounds.height); setToolTip(getToolTipFigure()); // Update the axis (for mapping the data points to display coordinates) _yAxis.setDisplaySize(_plotBounds.height); _xAxis.setDisplaySize(_plotBounds.width); _yAxisScale.refreshConstraints(); _xAxisScale.refreshConstraints(); } /** * Calculates the bounds of the y-axis label. * * @param bounds * the bounds within which the label will be displayed. These * bounds will be cropped to the remaining bounds. * @return the bounds of the label. */ private Rectangle calculateYAxisLabelBounds(final Rectangle bounds) { if (isYAxisLabeled()) { int width = yAxisLabelWidth(); Rectangle result = new Rectangle(bounds.x, bounds.y, width, bounds.height); bounds.crop(new Insets(0, width, 0, 0)); return result; } else { return ZERO_RECTANGLE; } } /** * Calculates the bounds of the x-axis label. * * @param bounds * the bounds within which the label will be displayed. These * bounds will be cropped to the remaining bounds. * @return the bounds of the label. */ private Rectangle calculateXAxisLabelBounds(final Rectangle bounds) { if (isXAxisLabeled()) { int height = TEXTHEIGHT; Rectangle result = new Rectangle(bounds.x, bounds.bottom() - height, bounds.width, height); bounds.crop(new Insets(0, 0, height, 0)); return result; } else { return ZERO_RECTANGLE; } } /** * Calculates the bounds of the plot of this figure. * * @param bounds * the bounds within which the plot will be displayed. Note: * unlike the other {@code calculate...} methods, this method * will not crop these bounds to the remaining bounds, because * the plot fills up all the remaining space (except necessary * padding). * @return the bounds of the plot. */ private Rectangle calculatePlotBounds(final Rectangle bounds) { int y = bounds.y + (showYAxis() ? TEXTHEIGHT / 2 : 0); // height, adjusted for extra space at top and bottom for y-axis labels int height = bounds.height - (showYAxis() ? (showXAxis() ? TEXTHEIGHT / 2 : TEXTHEIGHT) : 0); if (height < 0) { height = 0; } Rectangle result = new Rectangle(bounds.x, y, bounds.width, height); return result; } /** * Calculates the bounds of the x-axis of this figure. * * @param bounds * the bounds within which the x-axis will be displayed. These * bounds will be cropped to the remaining bounds. * @return the bounds of the x-axis. */ private Rectangle calculateXAxisBounds(final Rectangle bounds) { if (showXAxis()) { int height = xAxisHeight(); Rectangle result = new Rectangle( bounds.x + yAxisWidth() + yAxisLabelWidth(), bounds.bottom() - height, bounds.width - yAxisWidth() - yAxisLabelWidth(), height); bounds.crop(new Insets(0, 0, height, 0)); return result; } else { return ZERO_RECTANGLE; } } /** * Calculates the width of the y-axis label. * * @return the width of the y-axis label. */ private int yAxisLabelWidth() { return isYAxisLabeled() ? TEXTWIDTH : 0; } /** * Calculates the bounds of the y-axis of this figure. * * @param bounds * the bounds within which the y-axis will be displayed. These * bounds will be cropped to the remaining bounds. * @return the bounds of the y-axis. */ private Rectangle calculateYAxisBounds(final Rectangle bounds) { if (showYAxis()) { int width = yAxisWidth(); // height, adjusted for extra space at the bottom if the x-axis is // shown (the space is then already subtracted from the figureBounds) int height = bounds.height + (showXAxis() ? TEXTHEIGHT / 2 : 0); Rectangle result = new Rectangle(bounds.x, bounds.y, width, height); bounds.crop(new Insets(0, width, 0, 0)); return result; } else { return ZERO_RECTANGLE; } } /** * Calculates the bounds of the label of this figure. * * @param bounds * the bounds within which the label will be displayed. These * bounds will be cropped to the remaining bounds. * @return the bounds of the label. */ private Rectangle calculateLabelBounds(final Rectangle bounds) { if (isLabelled()) { int height = TEXTHEIGHT; Rectangle result = new Rectangle(bounds.x, bounds.y, bounds.width, height); bounds.crop(new Insets(height, 0, 0, 0)); return result; } else { return ZERO_RECTANGLE; } } /** * Returns whether this figure has a label. * * @return <code>true</code> if this figure has a label, * <code>false</code> otherwise. */ private boolean isLabelled() { return !"".equals(_waveformLabel.getText()); } /** * Returns whether this figure has a label on its x-axis. * * @return <code>true</code> if the x-axis is labeled, <code>false</code> * otherwise. */ private boolean isXAxisLabeled() { return showXAxis() && !"".equals(_xAxisLabel.getText()); } /** * Returns whether this figure has a label on its y-axis. * * @return <code>true</code> if the y-axis is labeled, <code>false</code> * otherwise. */ private boolean isYAxisLabeled() { return showYAxis() && !"".equals(_yAxisLabel.getText()); } /** * Calculates the width of the y-axis. * * @return the width of the y-axis in pixels. */ private int yAxisWidth() { if (showYAxis()) { return _labeledTicks ? AXIS_SIZE + TEXTWIDTH : AXIS_SIZE; } else { return 0; } } /** * Calculates the height of the x-axis. * * @return the height of the x-axis in pixels. */ private int xAxisHeight() { if (showXAxis()) { return _labeledTicks ? AXIS_SIZE + TEXTHEIGHT : AXIS_SIZE; } else { return 0; } } /** * Gets the IFigure for the tooltip. * * @return IFigure The IFigure for the tooltip */ private IFigure getToolTipFigure() { Panel panel = new Panel(); panel.setLayoutManager(new ToolbarLayout(false)); // panel.add(new Label("Count of data points: " + _data.length)); panel.add(new Label("Minimum value: " + lowestDataValue())); panel.add(new Label("Maximum value: " + greatestDataValue())); panel.setBackgroundColor(ColorConstants.tooltipBackground); return panel; } /** * A drawing style for drawing data points in a plot. * * @author Joerg Rathlev */ private enum DataPointDrawingStyle { /** * Draws a data point as a single pixel. */ PIXEL { /** * {@inheritDoc} */ @Override protected void draw(final Graphics g, final Point p) { g.drawPoint(p.x, p.y); } }, /** * Draws a data point as a small plus sign. */ SMALL_PLUS_SIGN { /** * {@inheritDoc} */ @Override protected void draw(final Graphics g, final Point p) { // # // # // ##### // # // # g.drawLine(p.x, p.y-2, p.x, p.y+2); g.drawLine(p.x-2, p.y, p.x+2, p.y); } }, /** * Draws a data point as a small square (3x3 pixels). */ SMALL_SQUARE { /** * {@inheritDoc} */ @Override protected void draw(final Graphics g, final Point p) { g.fillRectangle(p.x-1, p.y-1, 3, 3); } }, /** * Draws a diamod-shaped data point. */ DIAMOND { /** * {@inheritDoc} */ @Override protected void draw(final Graphics g, final Point p) { // # // ### // ##### // ### // # // Note: the call to drawPolygon is required because otherwise // for some reason the polygon drawn by fillPolygon is a bit // too small (the right edge is drawn one pixel to the left). g.drawPolygon(new int[] { p.x-2, p.y, p.x, p.y-2, p.x+2, p.y, p.x, p.y+2 }); g.fillPolygon(new int[] { p.x-2, p.y, p.x, p.y-2, p.x+2, p.y, p.x, p.y+2 }); } }; /** * Draws a data point at the specified coordinates. * * @param g the graphics object to use for drawing. * @param p the coordinates of the data point. */ protected abstract void draw(Graphics g, Point p); } /** * Receives data points from a subclass implementation and processes them. * * @author Joerg Rathlev */ protected interface IDataPointProcessor { /** * Processes the specified data point. The values of the data point must * be given in data value units (not display units). * * @param x * the x-value of the data point. * @param y * the y-value of the data point. */ void processDataPoint(double x, double y); } /** * Figure for the actual plot. */ private final class PlotFigure extends RectangleFigure { /** * The width of the lines of the graph. */ private int _plotLineWidth = 1; /** * The drawing style used for the data points. */ private DataPointDrawingStyle _style = DataPointDrawingStyle.SMALL_SQUARE; /** * {@inheritDoc} */ @Override public void paintFigure(final Graphics graphics) { Rectangle figureBounds = this.getBounds(); graphics.setForegroundColor(this.getForegroundColor()); graphics.drawLine(figureBounds.x, figureBounds.y, figureBounds.x, figureBounds.y + figureBounds.height); graphics.drawLine(figureBounds.x, figureBounds.bottom() - 1, figureBounds.x + figureBounds.width, figureBounds.bottom() - 1); for (int i = 0; i < _numberOfDataSeries; i++) { if (!_plotEnabled[i]) { continue; } // TODO: the points don't actually have to be recalculated everytime the plot // is redrawn -- only if the data points have changed or if the size of the // plot has changed. PointList pointList = calculatePlotPoints(i); graphics.setForegroundColor(_plotColor[i]); graphics.setBackgroundColor(_plotColor[i]); graphics.setLineWidth(_plotLineWidth); if (_lineChart) { graphics.drawPolyline(pointList); } for (int j = 0; j < pointList.size(); j++) { Point p = pointList.getPoint(j); _style.draw(graphics, p); } } } /** * Calculates the coordinates of the data points in the plot area. * * @param index * the index of the data series * @return a list of points to be plotted. */ private PointList calculatePlotPoints(final int index) { final Rectangle bounds = getBounds(); final PointList result = new PointList(); IDataPointProcessor proc = new IDataPointProcessor() { @Override public void processDataPoint(final double x, final double y) { if (_xAxis.isLegalValue(x) && _yAxis.isLegalValue(y)) { int displayY = valueToYPos(y); int displayX = valueToXPos(x); result.addPoint(bounds.x + displayX, bounds.y + displayY); } } }; dataValues(index, proc); return result; } /** * Sets the width of the lines of the plot. * @param lineWidth * The width of the lines of the graph. */ private void setPlotLineWidth(final int lineWidth) { _plotLineWidth = lineWidth; } /** * Sets the data point drawing style of this plot. * * @param style the style. */ private void setDataPointDrawingStyle(final int style) { switch(style) { case 0: _style = DataPointDrawingStyle.PIXEL; break; case 1: _style = DataPointDrawingStyle.SMALL_PLUS_SIGN; break; case 2: _style = DataPointDrawingStyle.SMALL_SQUARE; break; case 3: _style = DataPointDrawingStyle.DIAMOND; break; default: _style = DataPointDrawingStyle.SMALL_SQUARE; } } } /** * This class represents a scale. * * @author Kai Meyer */ private final class Scale extends RectangleFigure { /** * The direction of this Scale. */ private boolean _isHorizontal; /** * The Alignment for the Scalemarkers. */ private boolean _isTopLeft; /** * The lenght of the lines. */ private int _wideness = 10; /** * True, if the values of the Markers should be shown, false otherwise. */ private boolean _showValues = false; /** * The List of positive ScaleMarkers. */ private final List<ScaleMarker> _posScaleMarkers = new LinkedList<ScaleMarker>(); /** * Constructor. */ public Scale() { this.setLayoutManager(new XYLayout()); this.refreshConstraints(); // listen to figure movement events addFigureListener(new FigureListener() { @Override public void figureMoved(final IFigure source) { refreshConstraints(); } }); } /** * Refreshes the Constraints. */ private void refreshConstraints() { if ((this.getBounds().height==0) || (this.getBounds().width==0)) { _posScaleMarkers.clear(); this.removeAll(); return; } int index = 0; if (_isHorizontal) { int height = _wideness; if (_showValues) { height = TEXTHEIGHT + _wideness; } int distance = TEXTWIDTH; List<Tick> ticks = _xAxis.calculateIntegerTicks(distance, 3); for (Tick tick : ticks) { if (index >= _posScaleMarkers.size()) { this.addScaleMarker(index, _posScaleMarkers); } int x = valueToXPos(tick.value()); this.setConstraint(_posScaleMarkers.get(index), new Rectangle(x - (TEXTWIDTH/2), 0, TEXTWIDTH, height)); this.refreshScaleMarker(_posScaleMarkers.get(index), tick.value(), _showValues); index++; } this.removeScaleMarkers(index, _posScaleMarkers); } else { int width = _wideness; if (_showValues) { width = TEXTWIDTH + _wideness; } int distance = TEXTHEIGHT * 2; List<Tick> ticks = _yAxis.calculateTicks(distance, 3); for (Tick tick : ticks) { if (index >= _posScaleMarkers.size()) { this.addScaleMarker(index, _posScaleMarkers); } int y = valueToYPos(tick.value()); this.setConstraint(_posScaleMarkers.get(index), new Rectangle(0, y, width, TEXTHEIGHT)); this.refreshScaleMarker(_posScaleMarkers.get(index), tick.value(), _showValues); index++; } this.removeScaleMarkers(index, _posScaleMarkers); } } /** * Refreshes the given ScaleMarker. * @param marker * The ScaleMarker, which should be refreshed * @param labelValue * The new value for the displayed text * @param showValue * True, if the value should be shown, false otherwise */ private void refreshScaleMarker(final ScaleMarker marker, final double labelValue, final boolean showValue) { marker.setTopLeftAlignment(_isTopLeft); marker.setHorizontalOrientation(_isHorizontal); NumberFormat format = NumberFormat.getInstance(); format.setMaximumFractionDigits(2); marker.setText(format.format(labelValue)); marker.setShowValues(showValue); marker.setWideness(_wideness); } /** * Adds a new ScaleMarker into the given List at the given index. * @param index * The index * @param scaleMarkers * The List of ScaleMarkers */ private void addScaleMarker(final int index, final List<ScaleMarker> scaleMarkers) { ScaleMarker marker = new ScaleMarker(); scaleMarkers.add(index, marker); this.add(marker); } /** * Removes all ScaleMarkers in the given List, beginning by the given index. * @param index * The index * @param scaleMarkers * The List of ScaleMarkers */ private void removeScaleMarkers(final int index, final List<ScaleMarker> scaleMarkers) { if (!scaleMarkers.isEmpty() && (index<=scaleMarkers.size())) { while (index<scaleMarkers.size()) { this.remove(scaleMarkers.remove(index)); } } } /** * {@inheritDoc} */ @Override public void paintFigure(final Graphics graphics) { // graphics.setForegroundColor(ColorConstants.blue); // graphics.setBackgroundColor(ColorConstants.blue); // graphics.fillRectangle(this.getBounds()); } /** * Sets the orientation of this Scale. * * @param isHorizontal * The orientation of this Scale * (true=horizontal;false=vertical) */ public void setHorizontalOrientation(final boolean isHorizontal) { _isHorizontal = isHorizontal; this.refreshConstraints(); } /** * Sets the alignment for the ScaleMarker. * @param isTopLeft * The alignment for the ScaleMarker * (true=top/left; false=bottom/right) * */ public void setAlignment(final boolean isTopLeft) { _isTopLeft = isTopLeft; this.refreshConstraints(); } /** * Sets the wideness of this scale. * * @param wideness * The wideness of this scale */ public void setWideness(final int wideness) { _wideness = wideness; this.refreshConstraints(); } /** * Sets, if the values of the Markers should be shown. * @param showValues * True if the values of the Markers should be shown, false otherwise */ public void setShowValues(final boolean showValues) { _showValues = showValues; this.refreshConstraints(); } /** * {@inheritDoc} */ @Override public void setForegroundColor(final Color fg) { super.setForegroundColor(fg); for (ScaleMarker marker : _posScaleMarkers) { marker.setForegroundColor(fg); } } /** * This class represents a marker for the scale. * @author Kai Meyer */ private final class ScaleMarker extends RectangleFigure { /** * The Label of this ScaleMarker. */ private final Label _textLabel; /** * The hyphen of this ScaleMarker. */ private final ScaleHyphen _scaleHyphen; /** * The needed space of a {@link ScaleHyphen}. */ private final int _tickMarkSpace = 9; /** * The orientation of the scale to which this marker belongs. */ private boolean _isHorizontal; /** * The alignment of this Marker. */ private boolean _topLeft; /** * True, if the values of the Markers should be shown, false otherwise. */ private boolean _showValues = false; /** * Constructor. */ public ScaleMarker() { this.setLayoutManager(new XYLayout()); _textLabel = new Label(""); _textLabel.setForegroundColor(this.getForegroundColor()); _scaleHyphen = new ScaleHyphen(); _scaleHyphen.setForegroundColor(this.getForegroundColor()); this.add(_scaleHyphen); // if (_showValues) { this.add(_textLabel); // } this.refreshConstraints(); addFigureListener(new FigureListener() { @Override public void figureMoved(final IFigure source) { refreshConstraints(); } }); } /** * Recalculates the constraints. */ private void refreshConstraints() { Rectangle bounds = this.getBounds(); if (_isHorizontal) { // The tickmark height is the full height of this marker // figure if only the tickmark is shown, if the text label // is also shown, the height is the _tickMarkSpace. int tickmarkHeight = _showValues ? _tickMarkSpace : bounds.height; if (_topLeft) { this.setConstraint(_scaleHyphen, new Rectangle(0, bounds.height - tickmarkHeight, bounds.width, tickmarkHeight)); this.setConstraint(_textLabel, new Rectangle(0, 0, bounds.width, bounds.height-_tickMarkSpace)); } else { this.setConstraint(_scaleHyphen, new Rectangle(0, 0, bounds.width, tickmarkHeight)); this.setConstraint(_textLabel, new Rectangle(0, _tickMarkSpace, bounds.width, bounds.height-_tickMarkSpace)); } } else { int tickmarkWidth = _showValues ? _tickMarkSpace : bounds.width; if (_topLeft) { this.setConstraint(_scaleHyphen, new Rectangle(bounds.width - tickmarkWidth, 0, tickmarkWidth, bounds.height)); this.setConstraint(_textLabel, new Rectangle(0, 0, bounds.width-_tickMarkSpace, bounds.height)); } else { this.setConstraint(_scaleHyphen, new Rectangle(0, 0, tickmarkWidth, bounds.height)); this.setConstraint(_textLabel, new Rectangle(_tickMarkSpace, 0, bounds.width-_tickMarkSpace, bounds.height)); } } } /** * {@inheritDoc} */ @Override public void paintFigure(final Graphics graphics) { // graphics.setForegroundColor(ColorConstants.green); // graphics.setBackgroundColor(ColorConstants.green); // graphics.fillRectangle(this.getBounds()); } /** * Sets the orientation of the scale to which this marker belongs. * * @param isHorizontal * <code>true</code> if the scale is a horizontal scale * (i.e. along the x-axis), <code>false</code> if it is * a vertical scale. */ public void setHorizontalOrientation(final boolean isHorizontal) { _isHorizontal = isHorizontal; _scaleHyphen.setHorizontalOrientation(!isHorizontal); this.refreshLabel(); } /** * Sets the alignment of this figure. * * @param topLeft * The alignment of this figure * (true=top/left;false=bottom/right) */ public void setTopLeftAlignment(final boolean topLeft) { _topLeft = topLeft; _scaleHyphen.setAlignment(_topLeft); this.refreshLabel(); } /** * Sets the displayed text. * @param text * The text to display */ public void setText(final String text) { _textLabel.setText(text); this.refreshLabel(); } /** * Sets, if the values of the Markers should be shown. * @param showValues * True if the values of the Markers should be shown, false otherwise */ public void setShowValues(final boolean showValues) { _showValues = showValues; this.refreshLabel(); } /** * Sets the wideness of the Hyphen. * @param wideness * The wideness */ public void setWideness(final int wideness) { _scaleHyphen.setWideness(wideness); } /** * {@inheritDoc} */ @Override public void setForegroundColor(final Color fg) { super.setForegroundColor(fg); _scaleHyphen.setForegroundColor(fg); _textLabel.setForegroundColor(fg); } /** * Refreshes the Label. */ private void refreshLabel() { if (_showValues) { _textLabel.setVisible(true); if (_isHorizontal) { _textLabel.setTextPlacement(PositionConstants.WEST); if (_topLeft) { _textLabel.setTextAlignment(PositionConstants.BOTTOM); } else { _textLabel.setTextAlignment(PositionConstants.TOP); } } else { _textLabel.setTextPlacement(PositionConstants.NORTH); if (_topLeft) { _textLabel.setTextAlignment(PositionConstants.RIGHT); } else { _textLabel.setTextAlignment(PositionConstants.LEFT); } } } else { _textLabel.setVisible(false); } } /** * This class represents a hyphen for the scale. * * @author Kai Meyer */ private final class ScaleHyphen extends RectangleFigure { /** * The height of the line. */ private int _height = 0; /** * The width of the line. */ private int _width = 10; /** * The orientation of the line. Note that this will be * <code>true</code> for a <em>vertical</em> axis, which * gets horizontal lines as its tickmarks, and vice versa. */ private boolean _isHorizontal; /** * The wideness of this Hyphen. */ private int _wideness = 10; /** * The Alignment of this Hyphen. */ private boolean _isTopLeft; /** * {@inheritDoc} */ @Override public void paintFigure(final Graphics graphics) { graphics.setForegroundColor(this.getForegroundColor()); //vertical int x = this.getBounds().x+((int)(Math.round(((double)this.getBounds().width)/2))); int y = this.getBounds().y; if (_isHorizontal) { if (_isTopLeft) { x = this.getBounds().x + this.getBounds().width-_width; y = this.getBounds().y + this.getBounds().height/2; } else { x = this.getBounds().x; y = this.getBounds().y + this.getBounds().height/2; } } graphics.drawLine(x, y, x + _width, y + _height); } /** * Sets the wight and height of this Hyphen. */ private void setHeightAndWidth() { if (_isHorizontal) { _height = 0; _width = _wideness; } else { _height = _wideness; _width = 0; } } /** * Sets the orientation of this Hyphen. Note, this is the * orientation of the actual line that will be drawn, * <em>not</em> the orientation of the scale/axis! For a * vertical axis, which gets horizontal lines for its * tickmarks, this must be set to <code>true</code>, and * vice versa. * * @param isHorizontal * The Orientation of this Hyphen * true=horizontal; false = vertical */ public void setHorizontalOrientation(final boolean isHorizontal) { _isHorizontal = isHorizontal; this.setHeightAndWidth(); } /** * Sets the wideness of the Hyphen. * @param wideness * The wideness */ public void setWideness(final int wideness) { _wideness = wideness; this.setHeightAndWidth(); } /** * Sets the alignment of this Hyphen. * @param isTopLeft * The alignment (true=top/left; false = bottom/right) */ public void setAlignment(final boolean isTopLeft) { _isTopLeft = isTopLeft; } } } } }